package application.models.datalayer;

import application.models.exceptions.DataTypeException;

/** 
 * Describes entity attributes, by actual entity attribute values and data types of the values.
 * 
 * @author Philip Rasmussen & Morten Therkildsen
 */
public class Attribute {
	
	private String dataType;	// The data type of the entity attribute
	private String name;		// The name of the entity attribute
	private String dataSetOwner;	// The data set this attribute belongs to
	private boolean isMandatory;	// Whether or not this attribute is mandatory
	private Object value;		// The actual value of the attribute
	
	private int hashCodeCache = 0;
	
	/**
	 * A helper method that returns the class used to represent data types present in the data layer
	 * It only accepts 4 data types: BOOLEAN, INTEGER, STRING and DATE
	 * 
	 * @param dataType	The data type to receive a compatible class for
	 * @return			The class used to represent the data type in the application
	 * @throws DataTypeException	if the data type is unsupported or the conversion fails
	 */
	public static Class<?> getClassForDataType(String dataType) throws DataTypeException {

		if(dataType == null) throw new NullPointerException("dataType was null.");
		
		dataType = dataType.trim().toUpperCase();
		
		try{
			
			// NB! The returned classes must support a constructor accepting a single String parameter, which can recreate the object based on its toString() output
			// Implicit requirement =( The alternative is code duplication
			// A solution could be a data type class hierarchy.
			if(dataType.equals("BOOLEAN")) return Class.forName("java.lang.Boolean");
			if(dataType.equals("INTEGER")) return Class.forName("java.lang.Integer");
			if(dataType.equals("TEXT")) return Class.forName("java.lang.String");
			if(dataType.equals("DATE")) return Class.forName("application.models.datalayer.Date");
			else throw new DataTypeException("dataType does not match any data type in the data layer.");
			
		} catch(ClassNotFoundException e){throw new DataTypeException("The data type class could not be found.", e);
		}
	}
	
	/**
	 * Constructs a new Attribute instance
	 * 
	 * @param dataType	The data type of the value. Cannot be null or empty
	 * @param name		The name of the attribute Cannot be null of empty
	 * @param dataSetOwner		The data set the attribute belongs to. Cannot be null of empty
	 * @param isMandatory		Whether the attribute cannot be null (auto-increment counts as allowed to be null).
	 * @param value		The value of the attribute. May be null if the object is used to describe an attribute.
	 * @throw DataTypeException	if the passed value (if not null) does not match the passed data type
	 */
	public Attribute(String dataType, String name, String dataSetOwner, boolean isMandatory, Object value) throws DataTypeException {
		
		if(dataType == null) throw new NullPointerException("dataType was null.");
		if(name == null) throw new NullPointerException("name was null.");
		if(dataSetOwner == null) throw new NullPointerException("dataSetOwner was null.");
		
		// Check for illegal empty values
		if(dataType.trim().length() == 0) throw new IllegalArgumentException("dataType was empty.");
		if(name.trim().length() == 0) throw new IllegalArgumentException("name was empty.");
		if(dataSetOwner.trim().length() == 0) throw new IllegalArgumentException("dataSetOwner was empty.");
		
		this.dataType = dataType.trim().toUpperCase();
		this.name = name;
		this.dataSetOwner = dataSetOwner;
		this.isMandatory = isMandatory;
		
		if(value != null){
			if(!supportsValue(value)) throw new DataTypeException("The data type of value does not match dataType.");
			this.value = value;
		}
	}
	
	/**
	 * Returns the data type of the attribute
	 * @return The data type of the attribute
	 */
	public String getDataType(){
		
		return dataType;
	}
	
	/**
	 * Returns the name of the attribute
	 * @return The name of the attribute
	 */
	public String getName(){
	
		return name;
	}
	
	/**
	 * Returns the data set owner of the attribute
	 * @return The data set owner of the attribute
	 */
	public String getDataSetOwner(){
	
		return dataSetOwner;
	}
	
	/**
	 * Whether or not the attribute is mandatory
	 * @return True if it's mandatory, false if not
	 */
	public boolean isMandatory(){
		
		return isMandatory;
	}
	
	/**
	 * Returns the value of the attribute
	 * @return The value of the attribute
	 */
	public Object getValue(){
		
		return value;
	}
	
	/**
	 * Sets the value of the attribute
	 * @param The value the attribute should be set to
	 */
	public void setValue(Object value) throws DataTypeException {
		
		 if(!supportsValue(value)) throw new DataTypeException("The attribute value was of an unsupported data type.");
		 this.value = value;
		 
		 hashCodeCache = 0; // Clear cache
	}

	/**
	 * Tests whether a value can be assigned to this attribute (if the data type of the value matches a data type in the data layer)
	 * 
	 * @param value	The value to test
	 * @return 		true is the value is supported, false if not
	 */
	public boolean supportsValue(Object value){
		
		if(value == null) return true; // You may always clear the value
		
		try{ return getClassForDataType(getDataType()).isInstance(value);}
		catch(DataTypeException e){ return false;}
	}
	
	/**
	 * Whether some object is considered equal to this one
	 * 
	 * @param	the object to test equality with
	 * @return	true if they are considered equal, otherwise false
	 */
	public boolean equals(Object that){
		
		if(that == null || !(that instanceof Attribute)) return false;
		
		Attribute attr = (Attribute) that;
		
		if(
				(attr.getValue() == null ? value == null : attr.getValue().equals(value)) &&
				attr.getDataType().equals(dataType) &&
				attr.getName().equals(name) &&
				attr.isMandatory() == isMandatory &&
				attr.getDataSetOwner().equals(dataSetOwner)
			) return true;
		else return false;
	}
	
	/**
	 * We have overridden equals, now we have to do this...
	 * Code found at: http://bytes.com/topic/java/insights/723476-overriding-equals-hashcode-methods
	 */
    public int hashCode() {
    	
        final int multiplier = 23;
        
        if (hashCodeCache == 0) {
        	
            int code = 133;
            
            code = multiplier * code + dataType.hashCode();
            code = multiplier * code + name.hashCode();
            code = multiplier * code + dataSetOwner.hashCode();
            code = multiplier * code + (isMandatory ? 1 : 0);
            code = multiplier * code + ( value == null ? 0 : value.hashCode());
            hashCodeCache = code;
        }
        
        return hashCodeCache;
    }
}
